﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;

namespace Dycox.Drawing
{
    public class Track : IDisposable
    {
        TrackSegement[] _segments;
        GraphicsPath _trackPath;
        float _trackLength;

        private Track(GraphicsPath trackPath, bool closePath)
        {

            if (trackPath == null)
                throw new ArgumentNullException("trackPath");

            GraphicsPath path = (GraphicsPath)trackPath.Clone();

            path.Flatten();
            PointF[] points = path.PathPoints;

            path.Dispose();

            _trackPath = trackPath;

            int pointCount = points.Length;

            if (pointCount < 2) throw new ArgumentException("Invalid path.");

            if (closePath && points[0] != points[pointCount - 1])
            {
                Array.Resize(ref points, pointCount + 1);
                points[pointCount++] = points[0];
            }

            List<TrackSegement> list = new List<TrackSegement>(pointCount - 1);
            float length = 0f;

            for (int i = 0; i < pointCount - 1; i++)
            {
                TrackSegement info = TrackSegement.Create(points[i], points[i + 1], this, length);

                if (info != null)
                {
                    length += info.Length;
                    list.Add(info);
                }
            }
            _trackLength = length;
            _segments = list.ToArray();

        }

        public static Track CreateLine(PointF pt1, PointF pt2)
        {
            return CreateLine(new PointF[] { pt1, pt2 });
        }

        public static Track CreateLine(params PointF[] points)
        {
            if (points == null)
                throw new ArgumentNullException("points");
            if (points.Length < 2)
                throw new ArgumentException("Should have at least 2 points.");

            GraphicsPath path = new GraphicsPath();
            path.AddLines(points);
            return new Track(path, false);
        }

        public static Track CreateRectangle(RectangleF rect)
        {
            GraphicsPath path = new GraphicsPath();
            path.AddRectangle(rect);
            return new Track(path, true);
        }

        public static Track CreateEllipse(RectangleF rect)
        {
            GraphicsPath path = new GraphicsPath();
            path.AddEllipse(rect);
            return new Track(path, true);
        }

        public static Track CreateBezier(PointF pt1, PointF pt2, PointF pt3, PointF pt4)
        {
            GraphicsPath path = new GraphicsPath();
            path.AddBezier(pt1, pt2, pt3, pt4);
            return new Track(path, false);
        }

        public static Track Create(GraphicsPath path)
        {
            if (path == null)
                throw new ArgumentNullException("path");
            return new Track((GraphicsPath)path.Clone(), false);
        }

        public GraphicsPath GetTrackPath()
        {
            return (GraphicsPath)_trackPath.Clone();
        }

        public float TrackLength
        {
            get { return _trackLength; }
        }

        public void Dispose()
        {
            if (_trackPath != null)
            {
                _trackPath.Dispose();
            }

            foreach (var info in _segments)
                info.Dispose();
        }

        private string _text;

        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }

        private float _textPosition = 0.5f;

        public float TextPosition
        {
            get { return _textPosition; }
            set
            {
                if (value < 0 || value > 1f)
                    throw new ArgumentOutOfRangeException();

                _textPosition = value;
            }
        }

        private Font _font;

        public Font Font
        {
            get { return _font; }
            set { _font = value; }
        }

        private Color _textForeColor = SystemColors.WindowText;

        public Color TextForeColor
        {
            get { return _textForeColor; }
            set { _textForeColor = value; }
        }

        private Color _textBackColor;

        public Color TextBackColor
        {
            get { return _textBackColor; }
            set { _textBackColor = value; }
        }


        private Color _trackColor = SystemColors.WindowText;

        public Color TrackColor
        {
            get { return _trackColor; }
            set { _trackColor = value; }
        }

        private float _trackWidth = 1f;

        public float TrackWidth
        {
            get { return _trackWidth; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException();

                _trackWidth = value;
            }
        }

        public bool HitTest(PointF location, Graphics graphics)
        {
            Pen pen = new Pen(Color.Black, _trackWidth);
            if (_trackPath.IsOutlineVisible(location, pen, graphics))
            {
                // TODO: Implement hit test on track text block.
                return true;
            }

            pen.Dispose();
            return false;
        }

        private LineCap _startCap;

        public LineCap StartCap
        {
            get { return _startCap; }
            set { _startCap = value; }
        }

        private LineCap _endCap;

        public LineCap EndCap
        {
            get { return _endCap; }
            set { _endCap = value; }
        }

        private void DrawTrack(Graphics g)
        {
            Pen pen = CreatePen();

            //pen.DashStyle = DashStyle.Dash;

            g.DrawPath(pen, _trackPath);
            pen.Dispose();
        }

        private Pen CreatePen()
        {
            Pen pen = new Pen(_trackColor, _trackWidth);

            if (_startCap != default(LineCap))
            {
                if (_startCap == LineCap.Triangle || _startCap == LineCap.ArrowAnchor)
                {
                    pen.StartCap = LineCap.Custom;
                    pen.CustomStartCap = CreateArrowLineCap(_trackWidth, _endCap == LineCap.Triangle);
                }
                else
                    pen.StartCap = _startCap;
            }

            if (_endCap != default(LineCap))
            {
                if (_endCap == LineCap.ArrowAnchor || _endCap == LineCap.Triangle)
                {
                    pen.EndCap = LineCap.Custom;
                    pen.CustomEndCap = CreateArrowLineCap(_trackWidth, _endCap == LineCap.Triangle);
                }
                else
                    pen.EndCap = _endCap;
            }

            pen.DashStyle = _dashStyle;
            

            return pen;
        }

        private DashStyle _dashStyle;

        public DashStyle DashStyle
        {
            get { return _dashStyle; }
            set { _dashStyle = value; }
        }


        public void Draw(Graphics g)
        {
            DrawTrack(g);

            if (!string.IsNullOrEmpty(_text))
            {
                DrawText(g);
            }
        }

        private static CustomLineCap CreateArrowLineCap(float penWidth, bool triangle)
        {
            AdjustableArrowCap cap = new AdjustableArrowCap(4, 5);
            if (!triangle) cap.MiddleInset = 2;
            return cap;

            /*
            GraphicsPath path = new GraphicsPath();
            //path.FillMode = FillMode.Alternate;
            float bpx = 3f, bpy = 4f;
            float fpy = 0f;
            float inset = 3f;

            if (triangle)
                bpy = inset;

            path.StartFigure();
            //path.AddLines(new PointF[] {
            //    new PointF(-1f, -inset),
            //    new PointF(-bpx, -bpy),
            //    new PointF(0f, fpy),
            //    new PointF(bpx, -bpy),
            //    new PointF(1f, -inset)
            //});

            path.AddLines(new PointF[] {
                //new PointF(0, -inset  + 1),
                new PointF(-bpx, -inset),
                new PointF(0, 0),
                new PointF(bpx, -inset)
            });

            path.CloseFigure();

            if (penWidth < 2) inset += 2.5f - penWidth;
            //inset *= 2f ;
            return new CustomLineCap(path, null, LineCap.ArrowAnchor, inset);
             * */
        }


        private void DrawText(Graphics g)
        {
            float x = _trackLength * _textPosition;
            var info = FindSegement(x, 0);

            if (info != null)
            {
                PointF pt = info.GetPointAt(x);
                Font font = _font ?? SystemFonts.DefaultFont;
                SizeF size = g.MeasureString(_text, font);

                RectangleF rect = new RectangleF(pt, size);
                rect.Offset(-size.Width / 2f, -size.Height / 2f);

                Brush br;

                if (!_textBackColor.IsEmpty)
                {
                    br = new SolidBrush(_textBackColor);
                    g.FillRectangle(br, rect);
                    br.Dispose();
                }

                br = new SolidBrush(_textForeColor);
                g.DrawString(_text, font, br, rect);
                br.Dispose();
            }
        }


        private TrackSegement FindSegement(float x, int start)
        {
            return _segments.FirstOrDefault(
                o => x >= o.TrackOffset && x < o.TrackOffset + o.Length
                    );
        }

        class TrackSegement : IDisposable
        {
            Track _owner;
            float _trackOffset;
            Matrix _transform;

            private TrackSegement()
            {
            }

            public override string ToString()
            {
                return string.Format("{0} ({1})", _trackOffset, _length);
            }


            public static TrackSegement Create(PointF start, PointF end, Track owner, float trackOffset)
            {
                float k = (float)(end.Y - start.Y) / (float)(end.X - start.X);

                if (float.IsNaN(k))
                    return null;

                TrackSegement info = new TrackSegement();
                info._startPoint = start;
                info._endPoint = end;


                float angle = (float)(Math.Atan(k) * (180.0 / Math.PI));
                if (end.X < start.X) angle += 180f;

                info._slope = k;
                info._angle = angle;
                info._owner = owner;

                float x = end.X - start.X, y = end.Y - start.Y;
                info._length = (float)Math.Sqrt(x * x + y * y);

                info._trackOffset = trackOffset;

                Matrix matrix = new Matrix();
                matrix.RotateAt(info._angle, info._startPoint);
                info._transform = matrix;

                return info;
            }

            public Matrix Transform
            {
                get { return _transform; }
            }

            public void Dispose()
            {
                _transform.Dispose();
            }

            public float TrackOffset
            {
                get { return _trackOffset; }
            }

            private float _length;

            public float Length
            {
                get { return _length; }
                set { _length = value; }
            }


            private PointF _startPoint;

            public PointF StartPoint
            {
                get { return _startPoint; }
            }

            private PointF _endPoint;

            public PointF EndPoint
            {
                get { return _endPoint; }
            }

            private float _slope;

            public float Slope
            {
                get { return _slope; }
            }

            private float _angle;

            public float Angle
            {
                get { return _angle; }
            }



            public PointF GetPoint(float percentage)
            {
                float x = _trackOffset + _length * percentage;
                return GetPoint(x);
            }

            internal PointF GetPointAt(float x)
            {
                PointF[] pt = new PointF[] { new PointF(_startPoint.X + x - _trackOffset, _startPoint.Y) };
                _transform.TransformPoints(pt);
                return pt[0];
            }

        }
    }
}
